• By: Alex Kwon
  • Email: alex.kwon [at] hudsonthames [dot] org

Online Portfolio Selection - Mean Reversion

OLPS Strategies

Benchmarks

Momentum

Mean Reversion

Pattern Matching

Abstract

Mean Reversion is an effective quantitative strategy based on the theory that prices will revert back to its historical mean. A basic example of mean reversion follows the benchmark of Constant Rebalanced Portfolio. By setting a predetermined allocation of weight to each asset, the portfolio shifts its weights from increasing to decreasing ones. This module will implement four types of mean reversion strategies: Passive Aggressive Mean Reversion, Confidence Weighted Mean Reversion, Online Moving Average Reversion, and Robust Median Reversion.

Through this notebook, the importance of hyperparameters is highlighted as the choices greatly affect the outcome of returns. A lot of the hyperparameters for traditional research has been chosen by looking at the data in hindsight, and fundamental analysis of each dataset and market structure is required to profitably implement this strategy in a real-time market scenario.

1. Passive Aggressive Mean Reversion

  • PAMR
  • PAMR-1
  • PAMR-2

2. Online Moving Average Reversion

  • OLMAR-1
  • OLMAR-2

3. Confidence Weighted Mean Reversion

  • CWMR-Var
  • CWMR-SD

4. Robust Median Reversion

5. Data Exploration

1. Passive Aggressive Mean Reversion

Passive Aggressive Mean Reversion alternates between a passive and aggressive approach to the current market conditions. The strategy can effectively prevent a huge loss and maximize returns by setting a threshold for mean reversion.

PAMR takes in a variable $\epsilon$, a threshold for the market condition. If the portfolio returns for the period are below $\epsilon$, then PAMR will passively keep the previous portfolio, whereas if the returns are above the threshold, the portfolio will actively rebalance to the less performing assets.

In a way, $\epsilon$ can be interpreted as the maximum loss for the portfolio. It is most likely that the asset that decreased in prices for the period will bounce back, but there are cases where some companies plummet in value. PAMR is an effective algorithm that will prevent huge losses in blindly following these assets.

PAMR defines a loss function:

$l_{\epsilon} (b; x_t)$

If the returns for the period are below the threshold, $\epsilon$ :

$l_{\epsilon} (b; x_t) = 0$.

For returns that are higher than $\epsilon$ :

$l_{\epsilon} (b; x_t) = b \cdot x_t - \epsilon$.

Typically $\epsilon$ is set at a value between 0 and 1 and closer to 1 as daily returns fluctuate around 1.

We will introduce three versions of Passive Aggressive Mean Reversion in this notebook: PAMR, PAMR-1, and PAMR-2.

1.1 PAMR

The first method is described as the following optimization problem:

$\:$
$b_{t+1} = \underset{b \in \Delta_m}{\arg\min} \frac{1}{2} \|b-b_t \|^2 \: \text{s.t.} \: l_{\epsilon}(b;x_t)=0$

With the original problem formulation and $\epsilon$ parameters, PAMR is the most basic implementation.

1.2 PAMR-1

PAMR-1 introduces a slack variable to PAMR.

$C$ is a positive parameter that can be interpreted as the aggressiveness of the strategy.

$\:$
$b_{t+1} = \underset{b \in \Delta_m}{\arg\min} \left\lbrace\frac{1}{2} \|b-b_t \|^2 + C\xi\right\rbrace \: \text{s.t.} \: l_{\epsilon}(b;x_t) \leq \xi \geq 0$

A higher C value indicates the affinity to a more aggressive approach.

1.2 PAMR-2

PAMR-2 contains a quadratic term to the original slack variable from PAMR-1.

$\:$
$b_{t+1} = \underset{b \in \Delta_m}{\arg\min} \left\lbrace\frac{1}{2} \|b-b_t \|^2 + C\xi^2 \right\rbrace \: \text{s.t.} \: l_{\epsilon}(b;x_t) \leq \xi$

By increasing the slack variable at a quadratic rate, the method regularizes portfolio deviations.

2. Confidence Weighted Mean Reversion

Extending from PAMR, Confidence Weighted Mean Reversion looks at the autocovariance across all assets. Instead of focusing on a single asset's deviation from the original price, CWMR takes in second-order information about the portfolio vector as well to formulate a set of weights.

For CWMR, we introduce $\sum$, a measure of anti-confidence in the portfolio weights, where a smaller value represents higher confidence for the corresponding portfolio weights.

The optimization problem therefore becomes:

$\:$
$(\mu_{t+1}, \Sigma_{t+1}) = \arg \min D_{KL}(N(\mu,\Sigma | N(\mu_t,\Sigma_t)))$
such that $Pr[b^{\top} \cdot x_t \leq \epsilon] \geq \theta$ and $\mu \in \Delta_m$

Here the problem can be interpreted as maximizing the portfolio confidence by minimizing $\Sigma$ given a confidence interval $\theta$ determined by the threshold, $\epsilon$.

CWMR has two variations to solve this optimization problem with CWMR-SD and CWMR-Var.

2.1 CWMR-Var

CWMR uses the Kullback-Leibler divergence to further formulate the optimization problem as following:

$\:$
$(\mu_{t+1}, \Sigma_{t+1}) = \arg \min \frac{1}{2} \left( \log(\frac{\det \Sigma_t}{\det \Sigma}) + Tr(\Sigma_t^{-1}\Sigma) + (\mu_t-\mu)^{\top}\Sigma_t^{-1}(\mu_t - \mu) \right)$
such that $\epsilon - \mu^{\top}\cdot x_t \geq \phi x_t^{\top} \Sigma x_t$, $\mu^{\top} \cdot \textbf{1} = 1$, and $\mu \geq 0$

2.1 CWMR-SD

The standard deviation method further assumes the PSD property of $\Sigma$ to refactor the equations as the following:

$\:$
$(\mu_{t+1},\Gamma_{t+1}) = \arg \min \frac{1}{2}\left(\log(\frac{\det \Gamma_t^2}{\det \Gamma^2}) +Tr(\Gamma_t^{-2}\Gamma^2) + (\mu_t - \mu)^{\top}\Gamma_t^{-2}(\mu_t -\mu) \right)$
such that $\epsilon - \mu^{\top}\cdot x_t \geq \phi || \Gamma x_t ||$, $\Gamma$ is a PSD, $\mu^{\top} \cdot \textbf{1} = 1$, and $\mu \geq 0$

For both CWMR-Var and CWMR-SD, the calculations involve taking the inverse of a sum of another inverse matrix. The constant calculations of matrix inversion is extremely unstable and makes the model prone to any outliers and hyperparameters.

3. Online Moving Average Reversion

Traditional mean reversion techniques have an underlying assumption that the next price relative is inversely proportional to the latest price relative; however, mean reversion trends are not limited to a single period. Unlike traditional reversion methods that rely on windows of just one, OLMAR looks to revert to the moving average of price data.

OLMAR proposes two different moving average methods: Simple Moving Average and Exponential Moving Average.

From these moving average methods, the strategy predicts the next period's price relative. Using this new prediction, the portfolio iteratively updates its new weights.

$b_{t+1} = b_t + \lambda_{t+1}(\tilde{x}_{t+1}-\bar x_{t+1}\textbf{1})$

$\lambda$ is the constant multiplier to the new weights, and it is determined by the deviation from $\epsilon$, the reversion threshold. The portfolio will look to rebalance itself to the underperforming assets only if the portfolio returns are lower than the $\epsilon$ value.

$\:$
$\lambda_{t+1} = max \left\lbrace 0, \frac{\epsilon-b_t \cdot \tilde{x}_{t+1}}{\|\tilde{x}_{t+1}-\bar x_{t+1} \textbf{1}\|^2}\right\rbrace$

3.1 OLMAR-1

OLMAR-1 utilizes simple moving average to predict prices.

$\tilde{x}_{t+1}(w) = \frac{SMA_t(w)}{p_t}$
$\:$
$= \frac{1}{w} \left(\frac{p_t}{p_t} + \frac{p_{t-1}}{p_t}+ \cdot \cdot \cdot + \frac{p_{t-w+1}}{p_t}\right)$
$\:$
$= \frac{1}{w} \left( 1+ \frac{1}{x_t}+ \cdot \cdot \cdot + \frac{1}{\odot^{w-2}_{i=0}x_{t-1}} \right)$

3.1 OLMAR-2

OLMAR-2 uses exponential moving average to predict prices.

$\tilde{x}_{t+1}(\alpha) = \frac{EMA_t(\alpha)}{p_t}$
$\:$
$= \frac{\alpha p_t+(1-\alpha)EMA_{t-1}(\alpha)}{p_t}$
$\:$
$= \alpha \textbf{1} + (1 - \alpha) \frac{EMA_{t-1}(\alpha)}{p_{t-1}}\frac{p_{t-1}}{p_t}$
$\:$
$= \alpha \textbf{1} + (1 - \alpha) \frac{\tilde{x_t}}{x_t}$

4. Robust Median Reversion

Robust Median Reversion extends the previous Online Moving Average Reversion by introducing L1 median of the specified windows. Instead of reverting to a moving average, RMR reverts to the L1 median estimator, which proves to be a more effective method of predicting the next period's price because financial data is inherently noisy and contains many outliers.

L1-median is calculated with the following equation:

$\mu = \underset{\mu}{\arg \min}\overset{k-1}{\underset{i=0}{\sum}}||p_{t-i} - \mu ||$

where $k$ is the number of historical price windows, and $\mu$ represents the predicted price.

The calculation of L1-median is computationally inefficient, so the algorithm will be using the Modified Weiszfeld Algorithm.

$\:$
$\hat{x}_{t+1} = T(\mu) = (1 - \frac{\eta(\mu)}{\gamma(\mu)})^+ \: \tilde{T}(\mu + \min(1,\frac{\eta{\mu}}{\gamma(\mu)})\mu$
$\:$
$\eta(\mu) = 1 \text{ if } \mu=\text{any price and } 0 \text{ otherwise}$
$\:$
$\gamma(\mu)=\left|\left|\underset{p_{t-i} \neq \mu}{\sum}\frac{p_{t-i}-\mu}{||p_{t-i}-\mu||}\right|\right|$
$\:$
$\tilde{T}(\mu)=\left\lbrace \underset{p_{t-i}\neq \mu}{\sum}\frac{1}{||p_{t-i}-\mu||}\right\rbrace^{-1}\underset{p_{t-i}\neq \mu}{\sum}\frac{p_{t-i}}{||p_{t-i}-\mu||}$

Then next portfolio weights will use the predicted price to produce the optimal portfolio weights.

$\:$
$b_{t+1} = b_{t} - \min \left \lbrace 0, \frac{\hat{x}_{t+1} b_t-\epsilon}{||\hat{x}_{t+1}-\bar{x}_{t+1}\cdot\textbf{1}||^2}\right \rbrace \cdot (\hat{x}_{t+1}-\bar{x}_{t+1}\cdot\textbf{1})$
In [1]:
import pandas as pd
import numpy as np
import pickle
import optuna
import matplotlib.pyplot as plt
import plotly.offline as py
import plotly.graph_objs as go
import plotly.io as pio
from plotly.subplots import make_subplots 
from mlfinlab.online_portfolio_selection import *
# pio.renderers.default = "svg" # Toggle for GitHub rendering

Import Data

More information on these datasets is available here.

In [2]:
msci = pd.read_csv('data/MSCI.csv', parse_dates=True, index_col='Date').dropna()[1:]
us_equity = pd.read_csv('data/US_Equity.csv', parse_dates=True, index_col='Date')
djia = pd.read_csv('data/DJIA.csv', parse_dates=True, index_col='Date')
nyse = pd.read_csv('data/NYSE.csv', parse_dates=True, index_col='Date')
sp500 = pd.read_csv('data/SP500.csv', parse_dates=True, index_col='Date')
tse = pd.read_csv('data/TSE.csv', parse_dates=True, index_col='Date')

5.1 NYSE: 1961-1984

In [3]:
# Load Optuna Study.
nyse_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/nyse.db')
nyse_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/nyse.db')
nyse_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/nyse.db')
nyse_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/nyse.db')
nyse_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/nyse.db')
nyse_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/nyse.db')
nyse_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/nyse.db')
nyse_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/nyse.db')

PAMR

In [4]:
fig = optuna.visualization.plot_slice(nyse_pamr0)
fig.update_layout(title_text="NYSE PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(nyse_pamr1)
fig.update_layout(title_text="NYSE PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(nyse_pamr2)
fig.update_layout(title_text="NYSE PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

PAMR results on NYSE indicate the dependency on epsilon value over agg(aggressiveness). A low value of epsilon, one that is close to 0, is ideal in this strategy and indicates the preference for passive mean reversion.

CWMR

In [5]:
fig = optuna.visualization.plot_slice(nyse_cwmrsd)
fig.update_layout(title_text="NYSE CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(nyse_cwmrvar)
fig.update_layout(title_text="NYSE CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

CWMR results vary over the intervals of confidence and epsilon. The majority of the trials returned around the same magnitude; however, for certain parameters, CWMR produced extremely high returns. The high returns have a common parameter of a high epsilon value of 1.

OLMAR

In [6]:
fig = optuna.visualization.plot_slice(nyse_olmar1)
fig.update_layout(title_text="NYSE OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(nyse_olmar2)
fig.update_layout(title_text="NYSE OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

OLMAR's returns are exponentially higher than other mean reversion techniques so far with magnitudes of $10^{18}$. OLMAR returns are more dependent on the alpha and window parameters that choose the moving averages rather than epsilon. Interestingly, a window of 23 was ideal for OLMAR-1, and an alpha of 0.35 was ideal for OLMAR-2. A tendency of mean reversion for a longer period appears for the NYSE dataset proven by the relatively lower value of alpha and window of closer to three weeks.

RMR

In [7]:
fig = optuna.visualization.plot_parallel_coordinate(nyse_rmr)
fig.update_layout(title_text="NYSE RMR for Epsilon of [1, 25], N_iter of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

From the graph, we can notice that darker lines indicate higher returns. The majority of the dark line goes through a high epsilon and low window value. The range for the number of iterations and epsilon is wide, but the results were primarily dependent on the window value of 2.

In [8]:
# Buy and Hold.
nyse_bah = BuyAndHold()
nyse_bah.allocate(nyse)

# Constant Rebalanced Portfolio.
nyse_crp = ConstantRebalancedPortfolio()
nyse_crp.allocate(nyse)
In [9]:
# PAMR.
nyse_pamr0_ = PassiveAggressiveMeanReversion(0, nyse_pamr0.best_params['epsilon'])

# PAMR-1.
nyse_pamr1_ = PassiveAggressiveMeanReversion(1, nyse_pamr1.best_params['epsilon'], nyse_pamr1.best_params['agg'])

# PAMR-2.
nyse_pamr2_ = PassiveAggressiveMeanReversion(2, nyse_pamr2.best_params['epsilon'], nyse_pamr2.best_params['agg'])

# CWMR-SD.
nyse_cwmrsd_ = ConfidenceWeightedMeanReversion(nyse_cwmrsd.best_params['confidence'], nyse_cwmrsd.best_params['epsilon'],'sd')

# CWMR-Var.
nyse_cwmrvar_ = ConfidenceWeightedMeanReversion(nyse_cwmrvar.best_params['confidence'], nyse_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
nyse_olmar1_ = OnlineMovingAverageReversion(1, nyse_olmar1.best_params['epsilon'], window=nyse_olmar1.best_params['window'])

# OLMAR-2.
nyse_olmar2_ = OnlineMovingAverageReversion(2, nyse_olmar2.best_params['epsilon'], alpha=nyse_olmar2.best_params['alpha'])

# RMR.
nyse_rmr_ = RobustMedianReversion(nyse_rmr.best_params['epsilon'], nyse_rmr.best_params['n_iteration'], nyse_rmr.best_params['window'])
In [10]:
# Allocate weights.
nyse_pamr0_.allocate(nyse);
nyse_pamr1_.allocate(nyse);
nyse_pamr2_.allocate(nyse);
nyse_cwmrsd_.allocate(nyse);
nyse_cwmrvar_.allocate(nyse);
nyse_olmar1_.allocate(nyse);
nyse_olmar2_.allocate(nyse);
nyse_rmr_.allocate(nyse);
/Users/alexkwon/Documents/GitHub/hudson-and-thames/mlfinlab/mlfinlab/online_portfolio_selection/mean_reversion/online_moving_average_reversion.py:112: FutureWarning:

Currently, 'apply' passes the values as ndarrays to the applied function. In the future, this will change to passing it as Series objects. You need to specify 'raw=True' to keep the current behaviour, and you can pass 'raw=False' to silence this warning

In [11]:
fig = go.Figure()
idx = nyse_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=nyse_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=nyse_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=nyse_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=nyse_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=nyse_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=nyse_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=nyse_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=nyse_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=nyse_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=nyse_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on NYSE', xaxis_title='Date', yaxis_title='Relative Returns', yaxis_type="log")
fig.show()

OLMAR-2 and RMR returns are significantly higher than any other strategies with astronomical returns of $10^{18}$.

5.2 DJIA: 2001-2003

In [12]:
# Load Optuna Study.
djia_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/djia.db')
djia_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/djia.db')
djia_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/djia.db')
djia_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/djia.db')
djia_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/djia.db')
djia_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/djia.db')
djia_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/djia.db')
djia_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/djia.db')

PAMR

In [13]:
fig = optuna.visualization.plot_slice(djia_pamr0)
fig.update_layout(title_text="DJIA PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(djia_pamr1)
fig.update_layout(title_text="DJIA PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(djia_pamr2)
fig.update_layout(title_text="DJIA PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

Similarly to NYSE, DJIA data is dependent on the epsilon parameter. A lower value of epsilon, closer to 0, produced higher returns regardless of aggressiveness.

CWMR

In [14]:
fig = optuna.visualization.plot_slice(djia_cwmrsd)
fig.update_layout(title_text="DJIA CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(djia_cwmrvar)
fig.update_layout(title_text="DJIA CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

CWMR produces a similar graph to NYSE's. Epsilon of 1 continues to be the ideal parameter; however, confidence is different for CWMR-Var and CWMR-SD. The best confidence value for the variance method is 1, whereas the standard deviation's parameter is closer to 0.

OLMAR

In [15]:
fig = optuna.visualization.plot_slice(djia_olmar1)
fig.update_layout(title_text="DJIA OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(djia_olmar2)
fig.update_layout(title_text="DJIA OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

OLMAR parameters suggest a similar trend where the epsilon parameter does not significantly affect results. A window of 8 represents a shorter mean reversion trend with the DJIA dataset, and a low alpha value of 0.1 indicates less importance on the current weights as a parameter for price prediction.

RMR

In [16]:
fig = optuna.visualization.plot_parallel_coordinate(djia_rmr)
fig.update_layout(title_text="DJIA RMR for Epsilon of [1, 25], N_iter of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

Higher returns were achieved from high epsilon, lower n_iterations, and a window range of 12 to 16. This is in contrast to the window range suggested by OLMAR-1, where 8 had the highest returns. A higher window value for RMR compared to OLMAR-1 indicate that the noise in price data is more effectively reduced with a wider range of window.

In [17]:
# Buy and Hold.
djia_bah = BuyAndHold()
djia_bah.allocate(djia)

# Constant Rebalanced Portfolio.
djia_crp = ConstantRebalancedPortfolio()
djia_crp.allocate(djia)
In [18]:
# PAMR.
djia_pamr0_ = PassiveAggressiveMeanReversion(0, djia_pamr0.best_params['epsilon'])

# PAMR-1.
djia_pamr1_ = PassiveAggressiveMeanReversion(1, djia_pamr1.best_params['epsilon'], djia_pamr1.best_params['agg'])

# PAMR-2.
djia_pamr2_ = PassiveAggressiveMeanReversion(2, djia_pamr2.best_params['epsilon'], djia_pamr2.best_params['agg'])

# CWMR-SD.
djia_cwmrsd_ = ConfidenceWeightedMeanReversion(djia_cwmrsd.best_params['confidence'], djia_cwmrsd.best_params['epsilon'],'sd')

# CWMR-Var.
djia_cwmrvar_ = ConfidenceWeightedMeanReversion(djia_cwmrvar.best_params['confidence'], djia_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
djia_olmar1_ = OnlineMovingAverageReversion(1, djia_olmar1.best_params['epsilon'], window=djia_olmar1.best_params['window'])

# OLMAR-2.
djia_olmar2_ = OnlineMovingAverageReversion(2, djia_olmar2.best_params['epsilon'], alpha=djia_olmar2.best_params['alpha'])

# RMR.
djia_rmr_ = RobustMedianReversion(djia_rmr.best_params['epsilon'], djia_rmr.best_params['n_iteration'], djia_rmr.best_params['window'])
In [19]:
# Allocate weights.
djia_pamr0_.allocate(djia);
djia_pamr1_.allocate(djia);
djia_pamr2_.allocate(djia);
djia_cwmrsd_.allocate(djia);
djia_cwmrvar_.allocate(djia);
djia_olmar1_.allocate(djia);
djia_olmar2_.allocate(djia);
djia_rmr_.allocate(djia);
In [20]:
fig = go.Figure()
idx = djia_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=djia_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=djia_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=djia_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=djia_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=djia_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=djia_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=djia_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=djia_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=djia_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=djia_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on DJIA', xaxis_title='Date', yaxis_title='Relative Returns')
fig.show()

Mean reversion strategies perform significantly better than other benchmarks. For a period of a continuous downtrend, OLMAR-2 and RMR each have triple and double returns when most other strategies are stagnant around 1. Mean reversion provides promising returns for a long bear market.

5.3 TSE: 1994-1998

In [21]:
# Load Optuna Study.
tse_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/tse.db')
tse_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/tse.db')
tse_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/tse.db')
tse_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/tse.db')
tse_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/tse.db')
tse_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/tse.db')
tse_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/tse.db')
tse_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/tse.db')

PAMR

In [22]:
fig = optuna.visualization.plot_slice(tse_pamr0)
fig.update_layout(title_text="TSE PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(tse_pamr1)
fig.update_layout(title_text="TSE PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(tse_pamr2)
fig.update_layout(title_text="TSE PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

TSE suggests a different ideal epsilon value from the two previous datasets. Instead of 0, the best epsilon value is 0.6. The strategy performed an aggressive strategy for portfolio returns that were above 0.6 and performed a passive strategy for periods with returns less than 0.6. The returns are also relatively less sensitive to epsilon value as models with epsilon value between 0.4 and 1 have similar returns.

CWMR

In [23]:
fig = optuna.visualization.plot_slice(tse_cwmrsd)
fig.update_layout(title_text="TSE CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(tse_cwmrvar)
fig.update_layout(title_text="TSE CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

CWMR parameters are very different from the previous datasets. For the standard deviation method, epsilon values have minimal effect on returns as the highest drive was the low value of confidence around 0.1. For the variance method, a confidence value of 0.5 displayed significantly higher returns with epsilon not having as much effect as it did on the standard deviation method.

OLMAR

In [24]:
fig = optuna.visualization.plot_slice(tse_olmar1)
fig.update_layout(title_text="TSE OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(tse_olmar2)
fig.update_layout(title_text="TSE OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

TSE also represents a longer mean reversion window of 23 and an alpha of 0.9. The difference between the two parameters is noteworthy as a high value of alpha indicates a stronger weighting for recent prices; however, the high window value of 23 suggests that the periods are reliant on a more longer-term.

RMR

In [25]:
fig = optuna.visualization.plot_parallel_coordinate(tse_rmr)
fig.update_layout(title_text="TSE RMR for Epsilon of [1, 25], N_iter of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

RMR results indicate that the most important parameter is the number of windows. A window of 2 had the highest returns regardless of epsilon and number of iterations. This is in line with the OLMAR-2 strategy that had the best returns with a high alpha value. A shorter window frame was a better indicator of future prices for the TSE dataset.

In [26]:
# Buy and Hold.
tse_bah = BuyAndHold()
tse_bah.allocate(tse)

# Constant Rebalanced Portfolio.
tse_crp = ConstantRebalancedPortfolio()
tse_crp.allocate(tse)
In [27]:
# PAMR.
tse_pamr0_ = PassiveAggressiveMeanReversion(0, tse_pamr0.best_params['epsilon'])

# PAMR-1.
tse_pamr1_ = PassiveAggressiveMeanReversion(1, tse_pamr1.best_params['epsilon'], tse_pamr1.best_params['agg'])

# PAMR-2.
tse_pamr2_ = PassiveAggressiveMeanReversion(2, tse_pamr2.best_params['epsilon'], tse_pamr2.best_params['agg'])

# CWMR-SD.
tse_cwmrsd_ = ConfidenceWeightedMeanReversion(tse_cwmrsd.best_params['confidence'], tse_cwmrsd.best_params['epsilon'],'sd')

# CWMR-VAR.
tse_cwmrvar_ = ConfidenceWeightedMeanReversion(tse_cwmrvar.best_params['confidence'], tse_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
tse_olmar1_ = OnlineMovingAverageReversion(1, tse_olmar1.best_params['epsilon'], window=tse_olmar1.best_params['window'])

# OLMAR-2.
tse_olmar2_ = OnlineMovingAverageReversion(2, tse_olmar2.best_params['epsilon'], alpha=tse_olmar2.best_params['alpha'])

# RMR.
tse_rmr_ = RobustMedianReversion(tse_rmr.best_params['epsilon'], tse_rmr.best_params['n_iteration'], tse_rmr.best_params['window'])
In [28]:
# Allocate weights.
tse_pamr0_.allocate(tse);
tse_pamr1_.allocate(tse);
tse_pamr2_.allocate(tse);
tse_cwmrsd_.allocate(tse);
tse_cwmrvar_.allocate(tse);
tse_olmar1_.allocate(tse);
tse_olmar2_.allocate(tse);
tse_rmr_.allocate(tse);
In [29]:
fig = go.Figure()
idx = tse_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=tse_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=tse_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=tse_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=tse_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=tse_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=tse_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=tse_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=tse_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=tse_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=tse_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on TSE', xaxis_title='Date', yaxis_title='Relative Returns',yaxis_type="log")
fig.show()

OLMAR-2 and RMR continue to lead in returns with OLMAR-1 being a distant third. Shorter mean reversion trends proved to be a successful strategy for TSE.

SP500: 1998-2003

In [30]:
# Load Optuna Study.
sp500_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/sp500.db')
sp500_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/sp500.db')
sp500_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/sp500.db')
sp500_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/sp500.db')
sp500_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/sp500.db')
sp500_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/sp500.db')
sp500_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/sp500.db')
sp500_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/sp500.db')

PAMR

In [31]:
fig = optuna.visualization.plot_slice(sp500_pamr0)
fig.update_layout(title_text="SP500 PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(sp500_pamr1)
fig.update_layout(title_text="SP500 PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(sp500_pamr2)
fig.update_layout(title_text="SP500 PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

Epsilon of 0 proved to have the highest return as aggressiveness continues to have minimal impact on our returns.

CWMR

In [32]:
fig = optuna.visualization.plot_slice(sp500_cwmrvar)
fig.update_layout(title_text="SP500 CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(sp500_cwmrsd)
fig.update_layout(title_text="SP500 CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

CWMR-Var had its highest and lowest returns at a confidence value of 0.45. CWMR continues to be sensitive to its hyperparameters as even the slightest deviations of parameters return vastly different returns for its models.

OLMAR

In [33]:
fig = optuna.visualization.plot_slice(sp500_olmar1)
fig.update_layout(title_text="SP500 OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(sp500_olmar2)
fig.update_layout(title_text="SP500 OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

For SP500, a window of 2 and an alpha of 0.4 produced the highest returns. OLMAR-1 returns vary for most parameter choices and do not indicate a strong trend in any direction; however, OLMAR-2 has a clear alpha value that maximizes returns and is more robust to different parameter choices in the range between 0.3 and 0.5.

RMR

In [34]:
fig = optuna.visualization.plot_parallel_coordinate(sp500_rmr)
fig.update_layout(title_text="SP500 RMR for Epsilon of [1, 25], N_iter of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

The darkest lines for RMR pass through high epsilon, a low number of iterations, and a window range between 4 and 8.

In [35]:
# Buy and Hold.
sp500_bah = BuyAndHold()
sp500_bah.allocate(sp500)

# Constant Rebalanced Portfolio.
sp500_crp = ConstantRebalancedPortfolio()
sp500_crp.allocate(sp500)
In [36]:
# PAMR.
sp500_pamr0_ = PassiveAggressiveMeanReversion(0, sp500_pamr0.best_params['epsilon'])

# PAMR-1.
sp500_pamr1_ = PassiveAggressiveMeanReversion(1, sp500_pamr1.best_params['epsilon'], sp500_pamr1.best_params['agg'])

# PAMR-2.
sp500_pamr2_ = PassiveAggressiveMeanReversion(2, sp500_pamr2.best_params['epsilon'], sp500_pamr2.best_params['agg'])

# CWMR-SD.
sp500_cwmrsd_ = ConfidenceWeightedMeanReversion(sp500_cwmrsd.best_params['confidence'], sp500_cwmrsd.best_params['epsilon'],'sd')

# CWMR-Var.
sp500_cwmrvar_ = ConfidenceWeightedMeanReversion(sp500_cwmrvar.best_params['confidence'], sp500_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
sp500_olmar1_ = OnlineMovingAverageReversion(1, sp500_olmar1.best_params['epsilon'], window=sp500_olmar1.best_params['window'])

# OLMAR-2.
sp500_olmar2_ = OnlineMovingAverageReversion(2, sp500_olmar2.best_params['epsilon'], alpha=sp500_olmar2.best_params['alpha'])

# RMR.
sp500_rmr_ = RobustMedianReversion(sp500_rmr.best_params['epsilon'], sp500_rmr.best_params['n_iteration'], sp500_rmr.best_params['window'])
In [37]:
# Allocate weights.
sp500_pamr0_.allocate(sp500);
sp500_pamr1_.allocate(sp500);
sp500_pamr2_.allocate(sp500);
sp500_cwmrsd_.allocate(sp500);
sp500_cwmrvar_.allocate(sp500);
sp500_olmar1_.allocate(sp500);
sp500_olmar2_.allocate(sp500);
sp500_rmr_.allocate(sp500);
In [38]:
fig = go.Figure()
idx = sp500_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=sp500_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=sp500_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=sp500_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=sp500_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=sp500_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=sp500_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=sp500_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=sp500_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=sp500_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=sp500_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on SP500', xaxis_title='Date', yaxis_title='Relative Returns',yaxis_type="log")
fig.show()

OLMAR-2 and RMR produce the highest returns as the rest of the strategies fail to increase by more than 2 times the original value.

MSCI: 1993-2020

In [39]:
# Load Optuna Study.
msci_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/djia.db')
msci_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/djia.db')
msci_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/djia.db')
msci_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/djia.db')
msci_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/djia.db')
msci_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/djia.db')
msci_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/djia.db')
msci_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/djia.db')

PAMR

In [40]:
fig = optuna.visualization.plot_slice(msci_pamr0)
fig.update_layout(title_text="MSCI PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(msci_pamr1)
fig.update_layout(title_text="MSCI PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(msci_pamr2)
fig.update_layout(title_text="MSCI PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

Aggressive mean reversion was less preferred for MSCI with a low epsilon value preferred.

CWMR

In [41]:
fig = optuna.visualization.plot_slice(msci_cwmrvar)
fig.update_layout(title_text="MSCI CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(msci_cwmrsd)
fig.update_layout(title_text="MSCI CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

For both sets, epsilon of 1 had the highest returns with low confidence for CWMR-SD and high confidence for CWMR-Var. CWMR continues to provide models that are extremely sensitive to hyperparameter choices.

OLMAR

In [42]:
fig = optuna.visualization.plot_slice(msci_olmar1)
fig.update_layout(title_text="MSCI OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(msci_olmar2)
fig.update_layout(title_text="MSCI OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

A window of 8 was optimal for mean reversion prediction for OLMAR-1, and OLMAR-2 had an optimal alpha value close to 0.1.

RMR

In [43]:
fig = optuna.visualization.plot_parallel_coordinate(msci_rmr)
fig.update_layout(title_text="MSCI RMR for Epsilon of [1, 25], N_iter of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

RMR values were highest for high epsilon, a low number of iteration, and a window range of 12 to 15. The relatively higher optimal window value is similar to the optimal parameter for OLMAR-2. Both indicate a longer mean reversion expectancy regarding price predictions.

In [44]:
# Buy and Hold.
msci_bah = BuyAndHold()
msci_bah.allocate(msci)

# Constant Rebalanced Portfolio.
msci_crp = ConstantRebalancedPortfolio()
msci_crp.allocate(msci)
In [45]:
# PAMR.
msci_pamr0_ = PassiveAggressiveMeanReversion(0, msci_pamr0.best_params['epsilon'])

# PAMR-1.
msci_pamr1_ = PassiveAggressiveMeanReversion(1, msci_pamr1.best_params['epsilon'], msci_pamr1.best_params['agg'])

# PAMR-2.
msci_pamr2_ = PassiveAggressiveMeanReversion(2, msci_pamr2.best_params['epsilon'], msci_pamr2.best_params['agg'])

# CWMR-SD.
msci_cwmrsd_ = ConfidenceWeightedMeanReversion(msci_cwmrsd.best_params['confidence'], msci_cwmrsd.best_params['epsilon'],'sd')

# CWMR-Var.
msci_cwmrvar_ = ConfidenceWeightedMeanReversion(msci_cwmrvar.best_params['confidence'], msci_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
msci_olmar1_ = OnlineMovingAverageReversion(1, msci_olmar1.best_params['epsilon'], window=msci_olmar1.best_params['window'])

# OLMAR-2.
msci_olmar2_ = OnlineMovingAverageReversion(2, msci_olmar2.best_params['epsilon'], alpha=msci_olmar2.best_params['alpha'])

# RMR.
msci_rmr_ = RobustMedianReversion(msci_rmr.best_params['epsilon'], msci_rmr.best_params['n_iteration'], msci_rmr.best_params['window'])
In [46]:
# Allocate weights.
msci_pamr0_.allocate(msci);
msci_pamr1_.allocate(msci);
msci_pamr2_.allocate(msci);
msci_cwmrsd_.allocate(msci);
msci_cwmrvar_.allocate(msci);
msci_olmar1_.allocate(msci);
msci_olmar2_.allocate(msci);
msci_rmr_.allocate(msci);
In [47]:
fig = go.Figure()
idx = msci_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=msci_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=msci_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=msci_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=msci_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=msci_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=msci_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=msci_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=msci_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=msci_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=msci_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on MSCI', xaxis_title='Date', yaxis_title='Relative Returns', yaxis_type="log")
fig.show()

Not too surprisingly, OLMAR-2 and RMR produce the highest returns by more than 100 times in returns compared to other strategies. OLMAR-1, which typically represented a robust model, actually failed to have positive returns and approached a portfolio value closer to 0.

US Equity: 2011-2020

In [48]:
# Load Optuna Study.
equity_pamr0 = optuna.load_study(study_name='pamr0', storage='sqlite:///stored/equity.db')
equity_pamr1 = optuna.load_study(study_name='pamr1', storage='sqlite:///stored/equity.db')
equity_pamr2 = optuna.load_study(study_name='pamr2', storage='sqlite:///stored/equity.db')
equity_cwmrsd = optuna.load_study(study_name='cwmrsd', storage='sqlite:///stored/equity.db')
equity_cwmrvar = optuna.load_study(study_name='cwmrvar', storage='sqlite:///stored/equity.db')
equity_olmar1 = optuna.load_study(study_name='olmar1_large', storage='sqlite:///stored/equity.db')
equity_olmar2 = optuna.load_study(study_name='olmar2_large', storage='sqlite:///stored/equity.db')
equity_rmr = optuna.load_study(study_name='rmr', storage='sqlite:///stored/equity.db')

PAMR

In [49]:
fig = optuna.visualization.plot_slice(equity_pamr0)
fig.update_layout(title_text="US Equity PAMR for Epsilon of [0, 1.5]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(equity_pamr1)
fig.update_layout(title_text="US Equity PAMR-1 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(equity_pamr2)
fig.update_layout(title_text="US Equity PAMR-2 for Epsilon of [0, 1.5] and Aggressiveness of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

Unlike the majority of the previous datasets, higher epsilon was preferred as there were more daily mean reversion trends with US equity data.

CWMR

In [50]:
fig = optuna.visualization.plot_slice(equity_cwmrvar)
fig.update_layout(title_text="US Equity CWMR-Var for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(equity_cwmrsd)
fig.update_layout(title_text="US Equity CWMR-SD for Epsilon of [0, 1] and Confidence of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

A confidence of 0.45 for both models produced the highest returns; however, the lack of clean pattern and direction for the parameters represent the difficulty of applying CWMR to real live trading environments.

OLMAR

In [51]:
fig = optuna.visualization.plot_slice(equity_olmar1)
fig.update_layout(title_text="US Equity OLMAR-1 for Epsilon of [2, 25] and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

fig = optuna.visualization.plot_slice(equity_olmar2)
fig.update_layout(title_text="US Equity OLMAR-2 for Epsilon of [2, 25] and Alpha of [0, 1]", yaxis_title_text="Relative Returns")
display(fig)

A window of 11 and alpha of 0.1 proved to have the highest returns. The simple moving average trend was closer to a week-long, and for the first time, OLMAR-2 actually has negative returns compared to its original portfolio value.

RMR

In [52]:
fig = optuna.visualization.plot_parallel_coordinate(equity_rmr)
fig.update_layout(title_text="US Equity RMR for Epsilon of [1, 25], n_iterations of [2, 1000], and Window of [2, 30]", yaxis_title_text="Relative Returns")
display(fig)

The range of dark lines over the entire range for epsilon and number of iterations indicate that the most important parameter is the number of windows. RMR had the highest returns for windows in range of 18 to 23.  This is a higher value compared to OLMAR-1's window of 8 and is an example where a longer window range was more important as a variable to predict future prices due to the noise in daily price data.

In [53]:
# Buy and Hold.
equity_bah = BuyAndHold()
equity_bah.allocate(us_equity)

# Constant Rebalanced Portfolio.
equity_crp = ConstantRebalancedPortfolio()
equity_crp.allocate(us_equity)
In [54]:
# PAMR.
equity_pamr0_ = PassiveAggressiveMeanReversion(0, equity_pamr0.best_params['epsilon'])

# PAMR-1.
equity_pamr1_ = PassiveAggressiveMeanReversion(1, equity_pamr1.best_params['epsilon'], equity_pamr1.best_params['agg'])

# PAMR-2.
equity_pamr2_ = PassiveAggressiveMeanReversion(2, equity_pamr2.best_params['epsilon'], equity_pamr2.best_params['agg'])

# CWMR-SD.
equity_cwmrsd_ = ConfidenceWeightedMeanReversion(equity_cwmrsd.best_params['confidence'], equity_cwmrsd.best_params['epsilon'],'sd')

# CWMR-Var.
equity_cwmrvar_ = ConfidenceWeightedMeanReversion(equity_cwmrvar.best_params['confidence'], equity_cwmrvar.best_params['epsilon'],'var')

# OLMAR-1.
equity_olmar1_ = OnlineMovingAverageReversion(1, equity_olmar1.best_params['epsilon'], window=equity_olmar1.best_params['window'])

# OLMAR-2.
equity_olmar2_ = OnlineMovingAverageReversion(2, equity_olmar2.best_params['epsilon'], alpha=equity_olmar2.best_params['alpha'])

# RMR.
equity_rmr_ = RobustMedianReversion(equity_rmr.best_params['epsilon'], equity_rmr.best_params['n_iteration'], equity_rmr.best_params['window'])
In [55]:
equity_pamr0_.allocate(us_equity);
equity_pamr1_.allocate(us_equity);
equity_pamr2_.allocate(us_equity);
equity_cwmrsd_.allocate(us_equity);
equity_cwmrvar_.allocate(us_equity);
equity_olmar1_.allocate(us_equity);
equity_olmar2_.allocate(us_equity);
equity_rmr_.allocate(us_equity);
In [56]:
fig = go.Figure()
idx = equity_bah.portfolio_return.index
fig.add_trace(go.Scatter(x=idx, y=equity_bah.portfolio_return['Returns'], name="Buy and Hold"))
fig.add_trace(go.Scatter(x=idx, y=equity_crp.portfolio_return['Returns'], name="CRP"))
fig.add_trace(go.Scatter(x=idx, y=equity_pamr0_.portfolio_return['Returns'], name="PAMR"))
fig.add_trace(go.Scatter(x=idx, y=equity_pamr1_.portfolio_return['Returns'], name="PAMR-1"))
fig.add_trace(go.Scatter(x=idx, y=equity_pamr2_.portfolio_return['Returns'], name="PAMR-2"))
fig.add_trace(go.Scatter(x=idx, y=equity_cwmrsd_.portfolio_return['Returns'], name="CWMR-SD"))
fig.add_trace(go.Scatter(x=idx, y=equity_cwmrvar_.portfolio_return['Returns'], name="CWMR-Var"))
fig.add_trace(go.Scatter(x=idx, y=equity_olmar1_.portfolio_return['Returns'], name="OLMAR-1"))
fig.add_trace(go.Scatter(x=idx, y=equity_olmar2_.portfolio_return['Returns'], name="OLMAR-2"))
fig.add_trace(go.Scatter(x=idx, y=equity_rmr_.portfolio_return['Returns'], name="RMR"))
fig.update_layout(title='Mean Reversion Strategies on US Equity', xaxis_title='Date', yaxis_title='Relative Returns')
fig.show()

OLMAR-1 and RMR were the best performing strategies with OLMAR-2 lagging much behind its peers.

Conclusion

Mean reversion can be formulated in multiple methods. From a simple constant rebalanced portfolio to a complex robust median reversion, the strategy can employ endless techniques to find the most profitable set of portfolio weights. The blog post covered a wide range of mean reversion strategies employed in MlFinLab’s newest Online Portfolio Selection module, and readers will be able to replicate results using the simple methods of the new module.

The next notebook will focus on Pattern Matching.

If you enjoyed reading this please remember to leave us a star on GitHub and become a sponsor on Patreon to have exclusive access to our Slack channel!

In [ ]: